package com.refusea.commons.http

import com.alibaba.fastjson2.JSON
import com.refusea.commons.http.Https.Method.DELETE
import com.refusea.commons.http.Https.Method.GET
import com.refusea.commons.http.Https.Method.HEAD
import com.refusea.commons.http.Https.Method.POST
import com.refusea.commons.http.Https.Method.PUT
import java.io.File
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.HttpCookie
import java.net.InetSocketAddress
import java.net.ProxySelector
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse
import java.net.http.HttpResponse.BodyHandlers
import java.nio.charset.StandardCharsets
import java.time.Duration


/**
 * @author Zhang Yan, 2023/8/29
 **/
class Https(
    connectTimeout: Long = 2_000L,
    readTimeout: Long = 5_000L,
    proxyPort: Int = 0,
    supportCookie: Boolean = false,
) {
    private val client = buildClient(connectTimeout, proxyPort, supportCookie)
    private val timeout = Duration.ofMillis(readTimeout)

    fun get(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
    ): HttpResponse<String> {
        val req = generateReq(GET, url, headers, parameters, null, timeout)
        return client.send(req, BodyHandlers.ofString())
    }

    fun head(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
    ): HttpResponse<String> {
        val req = generateReq(HEAD, url, headers, parameters, null, timeout)
        return client.send(req, BodyHandlers.ofString())
    }

    fun delete(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
    ): HttpResponse<String> {
        val req = generateReq(DELETE, url, headers, parameters, null, timeout)
        return client.send(req, BodyHandlers.ofString())
    }

    fun post(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
        body: Any?,
    ): HttpResponse<String> {
        val req = generateReq(POST, url, headers, parameters, body, timeout)
        return client.send(req, BodyHandlers.ofString())
    }

    /** post 数据, 返回 byte array **/
    fun post4stream(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
        body: Any?,
    ): HttpResponse<ByteArray> {
        val req = generateReq(POST, url, headers, parameters, body, timeout)
        return client.send(req, BodyHandlers.ofByteArray())
    }

    fun put(
        url: String,
        headers: List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
        body: ByteArray,
    ): HttpResponse<String> {
        val req = generateReq(PUT, url, headers, parameters, body, timeout)
        return client.send(req, BodyHandlers.ofString())
    }

    fun download(
        url: String,
        headers:
        List<Pair<String, Any>> = emptyList(),
        parameters: List<Pair<String, Any>> = emptyList(),
        // if path is not null, save the response to the path
        path: String? = null,
    ): HttpResponse<ByteArray> {
        val req = generateReq(GET, url, headers, parameters, null, timeout)
        val rsp = client.send(req, BodyHandlers.ofByteArray())
        if (rsp.success && path != null) {
            File(path).writeBytes(rsp.body())
        }
        return rsp
    }

    fun cookie(name: String): String? {
        return cookies().firstOrNull { it.name == name }?.value
    }

    fun cookies(): List<HttpCookie> {
        val cookie = client.cookieHandler()
        if (cookie.isPresent) {
            val manager = cookie.get() as CookieManager
            return manager.cookieStore.cookies
        }
        return emptyList()
    }

    companion object {

        @JvmStatic
        val JSON_HEADER = "Content-Type" to "application/json; charset=utf-8"

        @JvmStatic
        val JSON_HEADERS = listOf(JSON_HEADER)

        private fun buildClient(
            connectTimeout: Long, proxyPort: Int, supportCookie: Boolean,
        ): HttpClient {
            val builder = HttpClient.newBuilder()
            builder.connectTimeout(Duration.ofMillis(connectTimeout))
            if (proxyPort > 0) {
                builder.proxy(ProxySelector.of(InetSocketAddress("127.0.0.1", proxyPort)))
            } else {
                builder.proxy(ProxySelector.getDefault())
            }
            if (supportCookie) {
                builder.cookieHandler(CookieManager(null, CookiePolicy.ACCEPT_ALL))
            }
            builder.followRedirects(HttpClient.Redirect.NEVER)
            return builder.build()
        }


        private fun generateReq(
            method: Method,
            url: String,
            headers: List<Pair<String, Any>>,
            parameters: List<Pair<String, Any>>,
            body: Any?,
            timeout: Duration,
        ): HttpRequest {
            val builder = HttpRequest.newBuilder()
                .version(HttpClient.Version.HTTP_2)
                .timeout(timeout)
                .uri(URI.create(completeUrl(url, parameters)))

            headers.forEach {
                builder.setHeader(it.first, it.second.toString())
            }

            when (method) {

                GET -> builder.GET()
                DELETE -> builder.DELETE()
                HEAD -> builder.method(HEAD.name, BodyPublishers.noBody())

                PUT -> {
                    body as ByteArray
                    require(body.isNotEmpty())
                    builder.method("PUT", BodyPublishers.ofByteArray(body))
                }

                POST -> when (body) {
                    null -> builder.POST(BodyPublishers.noBody())
                    is String -> builder.POST(BodyPublishers.ofString(body))
                    is ByteArray ->
                        if (body.isEmpty()) {
                            builder.POST(BodyPublishers.noBody())
                        } else {
                            builder.POST(BodyPublishers.ofByteArray(body))
                        }

                    is List<*> ->
                        if (body.isEmpty()) {
                            builder.POST(BodyPublishers.noBody())
                        } else {
                            builder.POST(
                                BodyPublishers.ofString(
                                    body.filterIsInstance<Pair<*, *>>()
                                        .joinToString("&") {
                                            it.first.toString() + '=' +
                                                    URLEncoder.encode(
                                                        it.second.toString(),
                                                        StandardCharsets.UTF_8
                                                    )
                                        }
                                )
                            )
                        }

                    else -> builder.POST(BodyPublishers.ofString(JSON.toJSONString(body)))
                }

            }

            return builder.build()
        }

        private fun completeUrl(url: String, parameters: List<Pair<String, Any>>): String {
            return if (parameters.isEmpty()) {
                url
            } else {
                url + if (url.contains('?')) {
                    '&'
                } else {
                    '?'
                } + parameters.joinToString("&") {
                    it.first + '=' +
                            URLEncoder.encode(it.second.toString(), StandardCharsets.UTF_8)
                }
            }
        }

        val HttpResponse<*>.success: Boolean
            get() = statusCode() in 200..299
        val HttpResponse<*>.failure: Boolean
            get() = statusCode() !in 200..299

    }

    enum class Method {
        GET, POST, DELETE, HEAD, PUT
    }

}
